﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Reflection;
using stunnware.CRM.Fetch;

namespace LinqtoCRM
{
    internal class ProjectionBuilder : ExpressionVisitor
    {
        private bool _transform = false;
        private CrmQueryProvider _provider;
        private FetchStatement _stmt;
        private Expression _body;

        internal ProjectionBuilder(CrmQueryProvider provider, FetchStatement stmt, Expression e)
        {
            _provider = provider;
            _stmt = stmt;
            this.Visit(e);

            // There won't be any select call in some cases, for example only where
            if (_body == null)
            {
                Type t = e.Type;

				if (t.IsGenericType)
				{
                    Type queryable = t.GetInterface(typeof(IQueryable<>).Name);
                    t = t.GetGenericArguments().First();
				}

                _body = CreateCrmObjectAccess(t);
                _stmt.Entity.Columns.AllColumns = true;
            }
            else
            {
                LambdaExpression l = (LambdaExpression) QueryFormatter.StripQuotes(_body);
                _transform = true;
                _body = this.Visit(l.Body);
            }

            Projector = Expression.Lambda(_body).Compile();
        }

        private Expression CreateCrmObjectAccess(Type t)
        {
            Expression target = Expression.Constant(_provider);
            Expression typeParam = Expression.Constant(t);
            MethodInfo getCurrent = typeof(CrmQueryProvider).GetMethod("GetCurrentCrmObject", BindingFlags.NonPublic | BindingFlags.Instance);
            MethodCallExpression call = Expression.Call(target, getCurrent, new Expression[] { typeParam });
            return Expression.Convert(call, t);
        }

        protected override System.Linq.Expressions.Expression VisitMethodCall(System.Linq.Expressions.MethodCallExpression m)
        {
            if (m.Method.DeclaringType == typeof(Queryable) || m.Method.DeclaringType == typeof(Enumerable))
            {
                switch (m.Method.Name)
                {
                    case "Select":
                    case "SelectMany":
                        // We will get here multiple times with chained queries
                        if(_body == null)
                            _body = m.Arguments.Last();
                        break;
                    case "Join":
                    case "GroupJoin":
                        // We might not have select at all if there is no where condition
                        // so in that case take the expression from join
                        if(_body == null)
                            _body = m.Arguments[4];
                        break;
                }
            }
            return base.VisitMethodCall(m);
        }

        static private void AddColumn(FetchEntityBase e, string attr)
        {
            var cols = e.Columns.Columns;
            if (!cols.Contains(attr))
            {
                cols.Add(attr);
            }
        }

        protected override Expression VisitMemberAccess(MemberExpression m)
        {
            if(_transform)
            {
                Expression parent = m.Expression;
                FetchEntityBase e;

                // First we should hit these c.fullname type of things
                if (parent.NodeType == ExpressionType.MemberAccess || parent.NodeType == ExpressionType.Parameter)
                {
                    string name = parent.Type.Name;
                    e = _stmt.GetEntity(name);
                    if (e != null)
                    {
                        AddColumn(e, m.Member.Name);
                        return Expression.MakeMemberAccess(CreateCrmObjectAccess(parent.Type), m.Member);
                    }
                }

                // If we are here then we have full object select
                e = _stmt.GetEntity(m.Type.Name);
                if (e != null)
                {
                    e.Columns.AllColumns = true;
                    return CreateCrmObjectAccess(m.Type);
                }
            }

            return base.VisitMemberAccess(m);
        }

        protected override Expression VisitParameter(ParameterExpression p)
        {
            if (_transform)
            {
                FetchEntityBase e = _stmt.GetEntity(p.Type.Name);
                if (e != null)
                {
                    e.Columns.AllColumns = true;
                    return CreateCrmObjectAccess(p.Type);
                }
            }
            return base.VisitParameter(p);
        }

        internal Delegate Projector { get; private set; }
    }
}
